缘起
ngx.ctx
是 lua-nginx-module
提供的一个充满魔力的 Lua table,它可以存放任何我们想要存放的内容,生命周期贯穿整个 location
,也正因为生命周期局限在单个 location
里,所以当发生内部跳转(例如通过 ngx.exec
)之后,之前的 ngx.ctx
将被销毁。所以很多时候,我们不得不转而使用 ngx.var.VARIABLE
来替代 ngx.ctx
,例如我们需要在 log
阶段的时候收集之前准备好的字段,然后发送到日志服务器或者 nsq
等组件。
然而,事物总是具有两面性,`ngx.var.VARIABLE` 生命周期虽然贯穿于一个请求,但是其代价却更加昂贵,它具有计算 `hash` 值,查找 `hash` 表,分配内存等等操作,这相比于 `ngx.ctx` 实在是繁重得多了。通过观察火焰图,大量的使用 `ngx.var.VARIABLE` 已经成为了一个瓶颈。于是才有了对 `ngx.ctx`,或者说 `ngx.exec` 的一次 hack 过程。
<!-- more -->
ngx.ctx
既然要对 ngx.ctx
进行 hack,首先需要了解 ngx.ctx
的机制,事实上,ngx.ctx
就是一个普通的 Lua table,lua-nginx-module
创建一个 table 之后,将其存放在 Lua 的注册表里,利用 luaL_ref
来索引每个 ngx.ctx
,利用 luaL_unref
来解除索引。这个索引,是被存放在 lua-nginx-module
的模块上下文里的,也就是 ngx_http_lua_ctx_s::ctx_ref
这个成员变量。
为什么经过内部跳转,ngx.ctx 会被销毁
Nginx 核心在进行内部跳转的时候,会把对应请求所有的模块上下文全部清除,可以参考函数 ngx_http_internal_redirect
,所以 lua-nginx-module
的 ctx_ref
也会被销毁。在 lua-nginx-module
关于 ngx.exec
的源码里也可以看到对 ngx.ctx
的解索引过程。
Hack it
了解了它的机制之后,我们可以试着来绕过这种限制,既然 lua-nginx-module
利用一个数字来索引 ngx.ctx
,我们也可以主动创建一个索引,将它存在一个介质里,只要这个介质不随着内部跳转而消失即可(例如 Nginx 变量就是一个非常好的选择),等到内部跳转完成之后,第一时间将 ngx.ctx
恢复出来即可,下面来介绍下这个过程。
首先我们需要一个变量
set ctx_ref "";
设计一个函数,创建一个新的索引
function _M.stash_ngx_ctx()
local ctxs = registry.ngx_lua_ctx_tables
local ctx_ref = base.ref_in_table(ctxs, ngx.ctx)
ngx.var.ctx_ref = tostring(ctx_ref)
end
registry 就是 Lua 的注册表,通过下面的方法获得。
local debug = require "debug"
local registry = debug.getregistry()
所有请求的 ngx.ctx
放置在一张表里,这张表存放在注册表里,key 就是 "ngx_http_lua_ctx_tables"
,所以上述代码里的 ctxs
就是存放所有请求的 ngx.ctx
的那张表了。
local ctx_ref = base.ref_in_table(ctxs, ngx.ctx)
这行代码给 ngx.ctx
创建了一个新的索引,关于具体的细节,大家有兴趣可以查看 lua-resty-core
的 base.ref_in_table
,这个函数的原理和 luaL_ref
一致。
拿到索引之后,将它存放到我们的变量即可。至此,当前请求的 ngx.ctx
就存在 2 个索引了(一个索引由 lua-nginx-module
管理,另外一个则由我们自己管理)。
执行完内部跳转后,恢复跳转前的
ngx.ctx
function _M.apply_ngx_ctx()
local ctx_ref = tonumber(ngx.var.ctx_ref)
if not ctx_ref then
return
end
local ctxs = registry.ngx_lua_ctx_tables
local origin_ngx_ctx = ctxs[ctx_ref]
ngx.ctx = origin_ngx_ctx
local FREE_LIST_REF = 0
ctxs[ctx_ref] = ctxs[FREE_LIST_REF]
ctxs[FREE_LIST_REF] = ctx_ref
ngx.var.ctx_ref = ""
end
我们通过存放在变量的 ctx_ref 来得到执行内部跳转前的 ngx.ctx
表,接着需要把我们自己管理的这个索引解除,否则会造成严重的内存泄漏!
local FREE_LIST_REF = 0
ctxs[ctx_ref] = ctxs[FREE_LIST_REF]
ctxs[FREE_LIST_REF] = ctx_ref
这三行代码即完成了解索引(和 LuaL_unref 一直),这里简单解释下, LuaL_unref
管理索引的时候,用 0 这个 index 记录上一次解索引的 index(为 nil
则表示目前还没有过解索引的操作),所以上述两行代码,实际上就是在当前需要解索引的 index 处记录了上一次解索引的 index,然后在 0 下标处记录当前最新的 index,有点像链表。这样操作有什么好处呢?当下次需要产生索引的时候,可以首先检查 0 下标,看看是否有解过索引的位置,如果有,复用即可,否则需要返回 #table + 1
,所以利用这个 “链表”,可以避免很多 Lua table 扩大,导致内存拷贝,影响到性能。
后续
这两个函数的代码已经经过充分测试,目前已经运行在我们的一个项目当中。
另外,这类基础的 Hack 操作,不适合存放在业务态,由调用者自己控制,因为这两个函数必须成对调用,否则就会造成内存泄漏。
使用之后,强烈建议进行压测,确认没有内存泄漏的隐患。
如果你有更多的 idea,可以给我发送邮件(zchao1995@gmail.com)。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。